package com.functest.orm

import com.functest.util.OrmUtil
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * Provider class is one of central components of ORM. It's meant to be used directly to search for Object of &lt;T&gt; type in database<br>
 *     BaseProvider is meant to be subclassed (as it provides obly basic search capabilities), though, it can be used directly if required.
 * @author DavydovMD
 * Date: 09.04.13
 */
class BaseProvider<T> {
    Class<T> clazz;
    private final static Logger logger = LoggerFactory.getLogger(BaseProvider)
    MultiTableMapper mapper

    BaseProvider(Class<T> clazz, List<SingleDbTableMapper> mappers) {
        this.clazz = clazz
        this.mapper = new MultiTableMapper(mappers, clazz)
    }

    BaseProvider(Class<T> clazz, MultiTableMapper mapper) {
        this.clazz = clazz
        this.mapper = mapper
    }

    /**
     * Get query builder for mapper. It's considered as bad practice to use query() method directly,
     *   since Provider class should implement all frequently used functionality, required for seaching for objects
     * @return QueryBuilder
     */
    QueryBuilder query() {
        mapper.queryBuilder()
    }

    /**
     * Find and return object by its key field value
     * @param id - key field value
     * @return
     */
    T byId(def id) {
        logger.debug("byId($id)")
        try {
           byFieldValueFirst(mapper.keyField, id)
        } catch (NoSuchElementException ignored) {
            null
        }
    }

    /**
     * Returns untransformed data from database for object, found by key field value.
     * @param id
     * @return values from source database table before transformation. Important: contains columns, which are used in mappers!
     */
    Map byIdRaw(Object id){
       mapper.queryBuilder().allFields().where(mapper.keyField, ConditionOp.EQ, id).build().executeRaw().first()
    }

    /**
     * To be used in subclasses only to simplify object creation from QueryBuilder's result
     * @param rows
     * @return
     */
    protected List<T> fromTransformedResult(List<Map> rows) {
        List<T> res = []
        rows.each { Map row ->
            res << mapper.buildFromTranformed(row, clazz)
        }
        res
    }

    /**
     * Filters list of objects
     * @param listToFilter
     * @param filters
     * @return Filtered list of objects
     */
     List<T> filter(List<T> listToFilter, List<Condition> filters){
        List<T> res = []
         logger.debug("filter(listToFilter: <${listToFilter.size()} objects>, filters: $filters)")

        listToFilter.each{T elem ->
            boolean filteredOut = false
            for(Condition c in filters){
                def value =  elem."${c.field}"
                if (!c.apply(value, true)) {
                    filteredOut = true
                    break
                };
            }
            if (!filteredOut){
                res << elem
            }
        }
        res
    }

    /**
     * Find objects with specified ids passing in argument
     * @param ids List of sought-for ids
     * @return List of found objects with specified in ids list; empty list if no objects found
     */
    List<T> byIds(List ids) {
        logger.debug("byIds($ids)")
        List<T> res = []
        if (!mapper.isMultiColumn(mapper.keyField)) {
            List<Map> rows = query().allFields().where(mapper.keyField, ConditionOp.IN, ids).build().execute()
            rows.each { Map row ->
                res << mapper.buildFromTranformed(row, clazz) // It's assumed that
            }
        } else {
            //Multi-column values are not compatible with IN operator!
            ids.each {
                res << byId(it)
            }
        }
        res
    }



    /**
     * Important: eliminates duplicates!!!
     * @param column
     * @param rows
     * @return
     */
    static <T> List<T> getColumnValues(String column, List<Map> rows) {
        Set res = [] as Set
        rows.each {
            res << it.get(column)
        }
        new ArrayList(res)
    }


    /**
     * Might be used in some providers to get "disinct" set of values in specific field from list of transformed transformedRows
     * @param field field name
     * @param transformedRows
     * @return de-duplcated list of @field values
     */
    protected static <T> List<T> getFieldValues(String field, List<Map> transformedRows) {
        Set res = [] as Set
        transformedRows.each {
            Map<String, Object> row ->
                res << row.find {
                    (it.key == field) || (OrmUtil.lastToken(it.key) == OrmUtil.lastToken(field))
                }.value
        }
        new ArrayList(res)
    }



    /**
     * Obtains list of objects with specified @field @value
     *
     * @param field Name of field
     * @param value
     * @return List of objects; empty list if no objects found
     */
    List<T> byFieldValue(String field, Object value) {
        logger.debug("byFieldValue(field: ${field}, value: ${value})")
        Object compatibleValue = SqlMappingUtil.toCompatible(value)

        QueryBuilder queryBase = query().distinct().field(mapper.keyField)
        WhereClauseBuilder queryWithCondition

        queryWithCondition = queryBase.where(field, ConditionOp.EQ, compatibleValue)

        List<Map> rows = queryWithCondition.build().execute()
        List ids = getColumnValues(mapper.keyField, rows)
        byIds(ids)
    }

    /**
     * Obtains filtered list of values
     *
     * @param field Name of field
     * @param filters List of applied Conditions
     * @return List of values; empty list if no values found
     */
    List getFieldValuesUsingFilters(String field, List<Condition> filters) {
        logger.debug("getFieldValuesUsingFilter(field:${field}, filters: ${filters}")
        WhereClauseBuilder wBuilder = query().distinct().field(field).field(mapper.keyField).where()
        filters.each { Condition filter ->
            wBuilder = wBuilder.and(filter)
        }

        List<Map> rows = wBuilder.build().execute()
        getFieldValues(field, rows)
    }

    /**
     * Return key field values of objects, found using filters
     * @param filters - List of conditions, all of which must be met for row to match search criteria
     * @return List of key field values
     */
    List findIdsUsingFilters(List<Condition> filters) {
        getFieldValuesUsingFilters(mapper.keyField, filters)
    }

    /**
     * Search for our objects using list of conditions
     *
     * @param filters List of conditions, all of which must be met for row to match search criteria
     * @return List of objects; empty list if there are no objects found
     */
    List<T> usingFilters(List<Condition> filters) {
        logger.debug("usingFilters(${filters})")
        WhereClauseBuilder wBuilder = query().distinct().allFields().where()
        filters.each { Condition filter ->
            wBuilder = wBuilder.and(filter)
        }

        List<Map> rows = wBuilder.build().execute()
        fromTransformedResult(rows)
    }

    /**
     * Returns objects found by ids (aka key field values), returned by query
     * @param fieldOfInterest - name of field to be used to determine database and table
     * @param query GString query that MUST return id as it's fist column.
     * @return List of objects found ; empty list if there are no objects found
     */
    List<T> usingQuery(QueryExecutor queryExecutor) {
        logger.debug("usingQuery(queryExecutor: ${queryExecutor})")
        List<Map> rows = queryExecutor.execute()
        byIds(getColumnValues(mapper.keyField, rows))
    }

    /**
     * Returns object transformed from first row returned by specified query
     *
     * @param queryExecutor GString query executing in db
     * @return Object; null if no rows have returned by query
     */
    T usingQueryFirst(QueryExecutor queryExecutor) {
        logger.debug("usingQueryFirst(queryExecutor: ${queryExecutor})")
        Map row = queryExecutor.firstRow()
        if (row)
            byId(row.get(mapper.keyField))
        else
            null
    }


   List<T> usingFilteringClosures(List<Closure> fClosures){
       logger.debug("usingFilteringClosures($fClosures)")
       WhereClauseBuilder wb = query().distinct().allFields().where()
       fClosures.each{
           it.call(wb)
       }
       usingQuery(wb.build())
   }

    T usingFilteringClosuresFirst(List<Closure<WhereClauseBuilder>> fClosures){
        logger.debug("usingFilteringClosuresFirst($fClosures)")
        WhereClauseBuilder wb = query().distinct().allFields().where()
        fClosures.each{
            it.call(wb)
        }
        usingQueryFirst(wb.build())
    }

    /**
     * Returns first object found that matches condition field==value
     *
     * @param field (simple) field name
     * @param value Object containing sought-for value. Can't be null!
     * @return Found object; null if no rows returned
     */
    T byFieldValueFirst(String field, Object value) {
        logger.debug("byFieldValueFirst($field, $value)")
        Map row

        assert value!=null
// Todo: decide which behavior is more correct, since it's not that obvious
//        if (value == null)
//            row = query().distinct().allFields().where(Condition.of(field, ConditionOp.IS_NULL, field)).build().firstRow()
//        else
        row = query().distinct().allFields().where(field, ConditionOp.EQ, value).build().firstRow()

        if (row)
            fromTransformedResult([row]).first()
        else
            null
    }


}
